/**********************************************************************************
  其它说明：
   本文件用来实现Arduino对各种传感器的驱动和控制，
   其中与串口有关的函数，如Serial.print，都是用来实现ESP8266与Arduino通信的。
 **********************************************************************************/
#include <LiquidCrystal.h>          //这个库用来驱动LCD屏幕，但这个库好像没用到，它应该是用IO直接驱动的LCD。参考文档：https://www.arduino.cc/en/Reference/LiquidCrystal
#include <Servo.h>                  //这个库用来驱动舵机，参考文档：https://www.arduino.cc/reference/en/libraries/servo/
#include "DHT.h"                    //这个库用来驱动DHT11，注意，这个文件可能是需要自己下载源文件安装，而不是通过包管理器安装的。参考文档：https://github.com/adafruit/DHT-sensor-library
#include <Wire.h>                   //这个库用来驱动I2C总线的设备，参考文档：https://www.arduino.cc/en/reference/wire
#include "SoftwareSerial.h"         //这个库应该是用软件来模拟的串口通信，但现在应该没用到这个库。实现ESP8266与Arduino通信的。参考文档：https://www.arduino.cc/en/Reference/softwareSerial
#include <DS1302.h>                 //这个库是用来驱动DS1302时钟芯片的，参考文档：https://github.com/msparks/arduino-ds1302

/************************** LCD1602显示屏引脚定义 ************************************/
int LCD1602_RS=12;
int LCD1602_RW=11;
int LCD1602_EN=10;
int DB[] = {6, 7, 8, 9};
int h;
int t;
int DUO_OUT1 = A4;
int DUO_OUT2 = A5;
char count;
char num=0;
String sh,st;

/************************** DS1302时钟模块引脚定义 ************************************/
DS1302 rtc(3, 4, 5); //对应DS1302的RST,DAT,CLK

/**************************** 舵机模块引脚定义 ****************************************/
Servo m1;
Servo m2;
unsigned long lastSend;

/************************** DHT11温湿度模块引脚定义 ************************************/
#define DHTPIN 2
#define DHTTYPE DHT11
String temp1,temp2;
DHT dht(DHTPIN, DHTTYPE);   /*初始化DHT11传感器*/

//SoftwareSerial soft(, 13); // RX, TX    /*这里应该是调试的时候用的，现在应该用不到了*/

char msg[25]={0};
String str;
char j=0;

/**********************************************************************************
  函数名：initRTCTime
  函数说明：
   该函数用来初始化RTC时钟
 **********************************************************************************/
void initRTCTime(void)
{
  rtc.writeProtect(false);              //关闭写保护
  rtc.halt(false);                      //清除时钟停止标志
  Time t(2021, 5, 24, 10, 28, 10, 7);   //新建时间对象 最后参数位星期数据，周日为1，周一为2以此类推
  rtc.time(t);                          //向DS1302设置时间数据
}

/**********************************************************************************
  函数名：printTime
  函数说明：
   1.从DS1302获取当前时间数据
   2.将时间数据显示在LCD屏幕上
   3.判断喂食时间并对舵机进行处理
 **********************************************************************************/
void printTime()
{
  Time tim = rtc.time();                //从DS1302获取时间数据，保存在tim变量中
  char buf[20]={0};
  char i=0;
  String strr;

  /*snprintf函数用来进行格式控制，将时间数据格式化成xx-xx-xx这样的字符串后，再将该数据存入buf变量中*/
  snprintf(buf, sizeof(buf), "%02d-%02d-%02d", tim.hr, tim.min, tim.sec);
           
  /*将格式化好的数据赋值给strr字符串变量*/
  strr=buf;

  /******************** 接下来将时间数据显示在LCD屏幕上 **********************************/
  Lcd1602_Com(0xc0+8);                    /*0XC0指令用来开启LCD显示，并设置LCD起始光标位置*/
  Lcd1602_Data('0'+ tim.hr%100/10);       /*"tim.hr%100/10"表示小时的十位，加'0'表示使用ASCII码显示*/
  Lcd1602_Data('0'+ tim.hr%10);           /*"tim.hr%10"表示小时的个位*/
  Lcd1602_Data(':');
  Lcd1602_Data('0'+ tim.min%100/10);
  Lcd1602_Data('0'+ tim.min%10);
  Lcd1602_Data(':');
  Lcd1602_Data('0'+ tim.sec/10);
  Lcd1602_Data('0'+ tim.sec%10);

  /******************** 接下来将判断喂食时间并对舵机进行处理 **********************************/
  /*字符串匹配，比较一下当前时间strr是否等于设置的喂食时间
    temp1表示第一次喂食时间
    temp2表示第二次喂食时间
  */
  if((!strr.compareTo(temp1))||(!strr.compareTo(temp2)))
  {
    /*如果当前时间等于第一次喂食时间，或者当前时间等于第二次喂食时间，都会进入这个if里，执行下面的for循环语句*/
    for(i=0;i<90;i++)
    {
      /*m1和m2分别表示两个舵机，write函数用来控制舵机的旋转角度
        因此，当到了喂食时间时，这两个舵机就会旋转90度，开启此次喂食*/
      m1.write(i);
      m2.write(i);
      delay(10);
    }
     count=1;     /*表示正在喂食*/
     num=0;       /*清零此次喂食时间，开始累计喂食时间*/
  }
  /*这里的num表示舵机打开的时间，count表示此时舵机是否开启
    count=1表示当前舵机已打开，处于正在喂食状态；count=0表示舵机已关闭
    在loop函数里，如果舵机已打开，就会每隔1秒，使num的值加一，从未就可以根据num来控制舵机从打开到关闭的延时时间
    下面的if条件解释为：如果当前处于喂食状态(舵机开启中)，并且已经喂食时间(舵机开启时间)为6秒，则关闭舵机，停止此次喂食*/
    if((num==6)&&(count==1))
    {
      for(i=90;i>0;i--)
      {  /*关闭两个舵机，结束此次喂食*/
         m1.write(i);
         m2.write(i);
         delay(10);   
      }
        num=0;
        count=0;  
    }
 // Serial.println(buf);
}

/**********************************************************************************
  函数名：setup
  函数说明：
   1.初始化串口，用于与ESP8266通信
   2.初始化DHT11温湿度传感器
   3.设置两个舵机的引脚
   4.初始化LCD1602
   5.初始化DS1302时钟模块
 **********************************************************************************/
void setup() {
 int i = 0;
  Serial.begin(9600);           /*初始化串口*/
  //soft.begin(9600);
  dht.begin();                  /*初始化DHT11温湿度传感器*/
  // pinMode(DUO_OUT1,OUTPUT);
  // pinMode(DUO_OUT2,OUTPUT);
  m1.attach(DUO_OUT1);          /*设置舵机的控制引脚*/
  m2.attach(DUO_OUT2);
  lastSend = 0;

  for (i=6; i <= 12; i++)       /*初始化LCD1602的引脚状态*/
  {
    pinMode(i,OUTPUT);
    delay(10);
  }

  delay(100);
  lcd1602_init();               /*驱动LCD1602显示屏，进行设备初始化*/
  delay(100);
  Lcd1602_Com(0x01);            /*向LCD发送清屏指令*/
  // delay(100);
  // Lcd1602_Com(0x80);
  // Lcd1602_Data('H');
  // Lcd1602_Data('u');
  // Lcd1602_Data('m');
  // initRTCTime();             /*注意，这个地方将initRTCTime函数注释掉了，是因为该函数用来向时钟模块写入初始化时间数据，只要第一次烧录一次就够了，时钟模块有额外供电，以后烧录就不需要初始化了*/
}

/**********************************************************************************
  函数名：loop
  函数说明：
     1.每隔2.5秒获取一次温湿度数据，并将数据通过串口发送至ESP8266
     2.读取喂食时间，如果用户设置了喂食时间，则进行相应的处理
     3.获取当前时间，将当前时间显示在LCD屏幕上，并在到达喂食时间时对舵机进行处理
 **********************************************************************************/
void loop() {
  /*这里的millis函数用来获取从设备开机到当前的时间毫秒数，这个函数在ESPP8266的程序里也有
    这里用开机秒数减去lastSend，相当于一种差值的方式，控制每2.5秒进入一次if语句*/
  if ( millis() - lastSend > 2500 ) {
    
    getAndSendTemperatureAndHumidityData(); /*获取温湿度数据并将数据通过串口发送至ESP8266，由ESP8266将数据发到ONENET*/
    lastSend = millis();      /*将相差的秒数清零，重新计时2.5秒*/
  }

  /*这里使用available函数，从串口里读取数据
    他读取的数据是由ESP8266通过串口发送给Arduino的，
    而ESP8266的数据是从ONENET平台，通过订阅MQTT的"inTpic1"主题获取到的*/
  while(Serial.available()>0)
  {
       str+=(char)Serial.read();    /*这里读取到的数据是两次喂食时间，具体的喂食时间的数据格式需要看APP，因为是APP发布的该数据*/
       j=1;                         /*这里的J变量，用来控制喂食时间使能，当j=1时，表示用户通过手机设置了喂食时间*/
  }
  if(j!=0)
  {
     temp1=str.substring(0,8);      /*喂食时间的数据格式，根据这条语句可以推测出来*/
     temp2=str.substring(9,17);     /*第0到第8为表示第一次喂食时间；第9到第17位表示第二次喂食时间。即temp1表示第一次喂食时间，即temp2表示第二次喂食时间*/
     j=0;
  }
  else
  {
     str="";
  }
  delay(1000);
  printTime();                      /*获取当前时间，将当前时间显示在LCD屏幕上，并在到达喂食时间时对舵机进行处理*/
  if(count==1)                      /*count表示当前是否开启了喂食，即舵机是否已开启*/
  {
      num++;                        /*num用来控制每次喂食持续的时间*/
  }
}

/**********************************************************************************
  函数名：getAndSendTemperatureAndHumidityData
  函数说明：
   1.调用readHumidity和readTemperature这两个库函数，获取温湿度数据
   2.将温湿度数据显示在LCD屏幕上
   3.将温湿度数据通过串口发给ESP8266模块
 **********************************************************************************/
void getAndSendTemperatureAndHumidityData()
{
  /*题外话：你看这个函数里有很多被注释掉的串口发送语句(如Serial.println)，这是他当时调试的时候使用的，
          这些语句被注释掉是因为不需要去发送这些无用的数据，只需要本函数最后一句发送有效数据就可以了*/
  //Serial.println("Collecting temperature data.");

  // 大概250ms读取一次
  /************* 调用readHumidity和readTemperature这两个库函数，获取温湿度数据 ***************/
  h = dht.readHumidity();
  t = dht.readTemperature();
  /*isnan函数用来查看读取温湿度是否是失败的
    这里的isnan函数用来判断h和t，即湿度和温度数据是否为空
    如果数据为空，isnan函数返回值为true*/
  if (isnan(h) || isnan(t)) {
   // Serial.println("Failed to read from DHT sensor!");
    return;
  }
  
  //Serial.print("Humidity: ");
  //Serial.print(h);
  //Serial.print(" %\t");
  //Serial.print("Temperature: ");
  //Serial.print(t);
  //Serial.print(" *C ");
  
  /*********************** 接下来将温湿度数据显示在LCD屏幕上 **********************************/
  Lcd1602_Com(0x80);          /*这里的指令0X80表示开启LCD显示，并且LCD的光标位置在首位0的位置*/
  Lcd1602_Data('0'+t/10);     /*"t/10"表示温度的十位，加上'0'表示使用ASCII码实现数据，下面几句同理*/
  Lcd1602_Data('0'+t%10);
  Lcd1602_Data(0xb2);         /*注意这里的发送的是数据，0XB2对应的ASCII码是上标2，即平方。因为LCD分辨率低，所以这里的0xb2就相当于显示了摄氏度的那个圈*/
  Lcd1602_Data('C'); 

  Lcd1602_Com(0xc0);          /*这里的0XC0表示光标移动到第二行的开头位置*/
  Lcd1602_Data('0' + h/10);
  Lcd1602_Data('0' + h%10);
  Lcd1602_Data('%'); 

  /********************** 最后将温湿度数据通过串口发给ESP8266模块 *****************************/
  st=String(t);               /*变量t和h原本的变量类型为数字，通过String函数将其转换为字符串，分别放在st和sh中*/
  sh=String(h);
  //dtostrf(t,5,2,tmp);
  // Serial.print(tmp1);
  /*通过串口将数据发送给ESP8266模块
    这里需要注意：
    1.发送的数据开头加上了字符'a'，这相当于自定义了一种协议，报头为'a'，报文为后面的数据
    2.这里的数据协议与ESP8266中是一致的，即在ESP8266中，通过判断报头字母'a'，就可以判断该数据是否为温湿度数据*/
  Serial.print("a"+st+"_"+sh);
}

/***********************************************************************************************/
/************************ 下面的三个函数是LCD1602的驱动，不需要深入理解 ********************************/
/***********************************************************************************************/

/**********************************************************************************
  函数名：Lcd1602_Com
  函数说明：
   该函数用来向LCD1602写入控制指令
 **********************************************************************************/
void Lcd1602_Com(int command)
{
  int i,temp;
  digitalWrite( LCD1602_RS,LOW);
  digitalWrite( LCD1602_RW,LOW);
  digitalWrite( LCD1602_EN,LOW);
  
  temp=command & 0xf0;
  for (i=DB[0]; i <= 9; i++)
  {
   digitalWrite(i,temp & 0x80);
   temp <<= 1;
  }
  
  digitalWrite( LCD1602_EN,HIGH);
  delayMicroseconds(1);
  digitalWrite( LCD1602_EN,LOW);
  
  temp=(command & 0x0f)<<4;
  for (i=DB[0]; i <= 9; i++)
  {
   digitalWrite(i,temp & 0x80);
   temp <<= 1;
  }
  
  digitalWrite( LCD1602_EN,HIGH);
  delayMicroseconds(1); 
  digitalWrite( LCD1602_EN,LOW);
  delay(10);
}

/**********************************************************************************
  函数名：Lcd1602_Data
  函数说明：
   该函数用来向LCD1602写入数据
 **********************************************************************************/
void Lcd1602_Data(int dat)
{
       int i=0,temp;
       digitalWrite( LCD1602_RS,HIGH);
       digitalWrite( LCD1602_RW,LOW);
       digitalWrite( LCD1602_EN,LOW);
      
       temp=dat & 0xf0;
       for (i=DB[0]; i <= 9; i++)
       {
         digitalWrite(i,temp & 0x80);
         temp <<= 1;
       }
      
       digitalWrite( LCD1602_EN,HIGH);
       delayMicroseconds(1);
       digitalWrite( LCD1602_EN,LOW);
      
       temp=(dat & 0x0f)<<4;
       for (i=DB[0]; i <= 9; i++)
       {
         digitalWrite(i,temp & 0x80);
         temp <<= 1;
       }
      
       digitalWrite( LCD1602_EN,HIGH);
       delayMicroseconds(1); 
       digitalWrite( LCD1602_EN,LOW);
       delay(2);
}

/**********************************************************************************
  函数名：lcd1602_init
  函数说明：
   通过向LCD1602写入控制指令，来初始化该模块
 **********************************************************************************/
void lcd1602_init()
{
      delay(100);
      Lcd1602_Com(0x32);   delay(50);//将8位总线转为4位总线
      Lcd1602_Com(0x28);   delay(50);//在四位线下的初始化
      Lcd1602_Com(0x0c);  delay(50);//开显示不显示光标
      Lcd1602_Com(0x06);  delay(50);//写一个指针加1
      Lcd1602_Com(0x01);  delay(50);//清屏
      Lcd1602_Com(0x80);  delay(50);//设置数据指针起点  
}
/************************************* End of file ******************************************/
